Ajax 跨域请求方案

通过 XHR 实现 Ajax 通信的主要限制,来源于跨域安全策略(同源策略)。XHR 对象只能访问与包含它的页面同源(同一个协议,同一个域名,同一个端口)的资源。虽然这个策略确实可以预防某些恶意行为,但是,实现合理的跨域请求对于开发某些浏览器应用程序也是至关重要的。
下面介绍几种利用 Ajax 实现跨域请求的解决方案:

1. CORS 跨域资源共享

CORS 是一个 W3C 标准,是 Cross-Origin Resource Sharing 的首字母缩写,翻译成中文是“跨域资源共享”。
它定义了在必须进行跨域访问时,浏览器和服务器应该怎么沟通。

1.1基本原理

使用自定义的 HTTP 头部,让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

CORS 需要浏览器和服务器同时支持。目前所有浏览器都支持该功能,IE 不能低于 IE10。
CORS 通信与同源的 Ajax 通信,代码是一模一样的。不同的是,当浏览器发现 Ajax 跨源请求,就会 自动 添加一些 附加的头信息;有时,还会多出一些 附加的请求
所以,浏览器在发现这是一个跨域请求的时候会自动帮我们处理一些事,只要服务端提供支持,前端是不需要做额外的事情的。

浏览器将 CORS 请求分为两类:简单请求(simple request)和非简单请求(not-so-simple)。我们来详细介绍一下这两种请求:

1.2 简单请求

只要同时满足以下两大条件,就属于简单请求:

  1. 采用以下一种请求方法
    • HEAD
    • GET
    • POST
  2. HTTP 的请求头信息不超过一下的几个字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: 只限三个值(application/x-www-form-urlencoded, multipart/form-data, text/plain)

1.2.1 流程

对于简单请求,浏览器直接发出 CORS 请求,具体来说就是在请求头信息中添加一个 Origin 字段,用来说明本次请求来自哪个源(协议+域名+端口):

Origin: https://www.google.com

服务器根据这个 Origin 的值,决定是否同意这次请求。

1.2.2 请求成功

如果 Origin 指定的源,在服务器的许可范围内,服务器返回的响应中,会多出几个头信息字段:

Access-Control-Allow-Origin: https://www.google.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Paco

  1. Access-Control-Allow-Origin
    该字段是必须的,它的字段要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求。
  2. Access-Control-Allow-Credentials
    该字段是可选的,它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包含在 CORS 请求中,设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发送给服务器。这个值也只能设为 true,如果服务器不要 Cookie,需要删除这个字段。
    如果浏览器希望能带着 Cookie 一起发到服务器,就必须将 XHR 的 withCredentials 属性值设为 true

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;

    如果省略这个属性的设置,有的浏览器还是会带 Cookie 一起发送。这时,可以显式地关闭:

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = false;
  3. Access-Control-Expose-Headers
    该字段可选。CORS 请求时,XHR 对象只能读取到响应头的部分字段,如果需要拿到额外的字段,就需要在这个字段指定。

1.2.3 请求失败

如果 Origin 指定的源,不在服务器的许可范围内,那么服务器会返回一个正常的 HTTP 响应。浏览器没有在响应头中发现 Access-Control-Allow-Origin 这个字段,就知道出错了,从而抛出一个错误。这个错误能被 XHR 的 onerror 回调函数捕获。
这种错误无法根据 HTTP 状态码识别,回应的状态码有可能是200。

1.3 非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是 application/json.

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”(preflight)。
浏览器通过这次请求,询问服务器,当前网页所在的源是否在服务器的许可名单之中,以及可以使用哪些头信息字段。
只有得到肯定答复,浏览器才会发出正式的 XHR 请求,否则就报错。

var xhr = new XMLHttpRequest();
var url = "https://www.google.com";
xhr.withCredentials = false;
xhr.open("put", url, true);
xhr.setRequestHeader("test", "inform");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();

上面的代码中,url 是异源,而且方法是 PUT,Content-Type 字段的类型是 application/json,还增加了一个自定义字段。
浏览器发现,这是一个非简单 CORS 请求,就会自动发送一个预检请求,要求服务器确认可以这样请求。

一旦服务器通过了预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

1.4 优缺点

优点:

  1. 功能强大,支持所有的 HTTP 请求方法
  2. 开发者可以使用普通的 XHR 发起请求和获得数据
  3. 能有效地进行错误处理

缺点:

  1. 兼容性问题,只有现代浏览器才支持

2. JSONP

JSONP 是 JSON with padding 的缩写,中文可以翻译成“填充式 JSON”或“参数式 JSON”,是应用 JSON 的一种新方法。
JSONP 看起来和 JSON 差不多,只不过是被包含在回调函数中的 JSON。
JSONP 由两部分组成,回调函数和数据。

callback({"name": "Paco"});

回调函数的名字一般是在请求中指定的,而数据就是传入回调函数的 JSON。
下面是一个典型的 JSONP 请求:

http://www.google.com/json/?callback=handleResponse

上面的代码中通过查询字符串的形式指定了回调函数。

2.1 基本原理

浏览器请求脚本资源不受同源策略限制,并且请求到脚本资源后将其立即执行。

实现思路是,动态地向网页中添加一个 <script>元素,通过这个元素的 src 特性,向异源服务器请求 JSON 数据;服务器收到请求后,将数据作为参数,放在一个指定名字的回调函数里传回来。

2.2 浏览器端

发送一个 JSONP 请求,在浏览器端的操作如下:

  1. 注册一个全局回调函数,这个函数接收一个参数,参数是期望得到的服务器端返回数据,函数的具体内容是处理这个数据。
  2. 向网页中动态插入<script> 元素,由它向跨源网址发出请求
    以下代码展示了浏览器端的操作:
    function foo(data) {
    alert("your girlfriend likes " + data.present);
    }
    function addScriptTag(src) {
    var script = document.createElement("script");
    script.setAttribute("type", "text/javascript");
    script.setAttribute("src", src);
    document.body.appendChild(script);
    }
    window.onload = function() {
    addScriptTag("http://test.com/present?callback=foo");
    }

2.3 服务器端

异源服务器接收到这个请求后,会将数据放在回调函数的参数位置返回。

foo({"present": "cat"});

由于<script> 元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了 foo 函数,该函数就会立即被调用执行。
另外,作为参数的 JSON 数据被视为 JavaScript对象,而不是字符串,因此避免了 JSON.parse 的步骤。

2.4 优缺点

优点:

  1. 兼容性很好,所有浏览器都支持
  2. 简单易用,能直接访问响应文本,支持在浏览器和服务器之间双向通信
    缺点:
  3. 只支持 GET 方法
  4. 要确定 JSONP 请求是否失败并不容易。虽然 HTML5给 <script> 元素新增了一个 onerror 事件处理程序,但目前还没有得到浏览器支持。开发人员不得不使用计时器检测指定时间内是否接收到了响应。

3. 搭建中间转发层(代理服务器)

思路是,通过在同源域名的服务器端架设一个代理服务器转发网页的请求,来实现同源策略。
你从客户端向你的服务器端的代理发出请求,而不是直接向异源服务器发出请求,然后代理把请求传递到异源服务器上,随后也是由代理向你的客户端传递返回数据。
也就是说,你的客户端始终在和你的服务器端打交道,这就符合了同源策略。
缺点是,你需要在服务器端进行额外的开发,而且因为中间经过了转发,所以网络开销和性能负载都有影响。